/* Copyright (c) Microsoft Corporation.
   Licensed under the MIT License. */

/***************************************************************************

	tagman.cpp: Tag Manager class (TAGM)

	Primary Author: ******
	Review Status: REVIEWED - any changes to this file must be reviewed!

	It is important to keep in mind that there are two layers of caching
	going on in TAGM: Caching content from the CD (or other slow source)
	to the local hard disk, and caching resources in RAM using chunky
	resources (the CRF and CRM classes).

	For each source, TAGM maintains (in an SFS) a CRM (Chunky Resource
	Manager) of all the content	files on the source and a CRF (Chunky
	Resource File), which is a single file on the HD which can be used
	for faster access to the source.  Both CRFs and CRMs can cache
	resources in RAM.  Since Socrates copies *all* content from the CD
	to the cache file, the CRM is told not to cache its resources in
	RAM.  However, if the source is actually on the HD, TAGM notices and
	doesn't copy any content to a cache file, since that would be a waste
	of time.  Instead, the content is read directly from the CRM.  In this
	case, TAGM does tell the CRM to cache its resources in RAM.

	Source names: every source has a long and short name.  This is so we
	can use long names for the source directory on the HD (e.g., "3D Movie
	Maker"), and short names for the source directory on the CD 
	("3DMOVIE").  We have to support short names because CD-ROMs currently
	do not allow long filenames.  So everywhere that we look for the 
	source directory, we accept either the long or short name.  
	_pgstSource keeps track of these names.  Rather than have one GST for
	short names and one for long names, each string in _pgstSource is the
	"merged name", which is the long name followed by a slash character 
	(/) followed by the short name, e.g., "3D Movie Maker/3DMOVIE".  The
	SplitString() function splits the merged name into the long and
	short names.

***************************************************************************/
#include "soc.h"
ASSERTNAME

RTCLASS(TAGM)

const BOM kbomSid = 0xc0000000;

// Source File Structure...keeps track of known sources and caches
struct SFS
	{
public:
	long sid;			// ID for this source
	FNI fniHD;			// FNI of the HD directory
	FNI fniCD;			// FNI of the CD directory
	PCRM pcrmSource;	// CRM of files on the CD (or possibly HD)
	bool tContentOnHD;  // Is the content on the HD or CD?

public:
	void Clear(void)	// Zeros out an SFS
		{
		sid = ksidInvalid;
		fniHD.SetNil();
		fniCD.SetNil();
		pcrmSource = pvNil;
		tContentOnHD = tMaybe;
		}
	};


/***************************************************************************
	Initialize the tag manager
***************************************************************************/
PTAGM TAGM::PtagmNew(PFNI pfniHDRoot, PFNINSCD pfninscd, long cbCache)
{
	AssertPo(pfniHDRoot, ffniDir);
	Assert(pvNil != pfninscd, "bad pfninscd");
	AssertIn(cbCache, 0, kcbMax);

	PTAGM ptagm;

	ptagm = NewObj TAGM;
	if (pvNil == ptagm)
		goto LFail;	

	ptagm->_fniHDRoot = *pfniHDRoot;
	ptagm->_cbCache = cbCache;
	ptagm->_pfninscd = pfninscd;

	ptagm->_pglsfs = GL::PglNew(size(SFS));
	if (pvNil == ptagm->_pglsfs)
		goto LFail;	

	ptagm->_pgstSource = GST::PgstNew(size(long)); // extra data is sid
	if (pvNil == ptagm->_pgstSource)
		goto LFail;

	AssertPo(ptagm, 0);
	return ptagm;
LFail:
	ReleasePpo(&ptagm);
	return pvNil;
}


/***************************************************************************
	Tag Manager destructor
***************************************************************************/
TAGM::~TAGM(void)
{
	AssertBaseThis(0);

	long isfs;
	SFS sfs;

	if (pvNil != _pglsfs)
		{
		for (isfs = 0; isfs < _pglsfs->IvMac(); isfs++)
			{
			_pglsfs->Get(isfs, &sfs);
			ReleasePpo(&sfs.pcrmSource);
			}
		ReleasePpo(&_pglsfs);
		}
	ReleasePpo(&_pgstSource);
}


/***************************************************************************
	Split a merged string into its long and short components
***************************************************************************/
void TAGM::SplitString(PSTN pstnMerged, PSTN pstnLong, PSTN pstnShort)
{
	AssertPo(pstnMerged, 0);
	AssertVarMem(pstnLong);
	AssertVarMem(pstnShort);

	achar *pchStart = pstnMerged->Psz();
	achar *pch;

	for (pch = pchStart; *pch != chNil; pch++)
		{
		if (*pch == ChLit('/'))
			{
			pstnLong->SetRgch(pchStart, (pch - pchStart));
			pstnShort->SetSz(pch + 1);
			return;
			}
		}
	// no slash, so set both pstnLong and pstnShort to pstnMerged
	*pstnLong = *pstnMerged;
	*pstnShort = *pstnMerged;
}


/***************************************************************************
	Return source title string table so it can be embedded in documents
***************************************************************************/
PGST TAGM::PgstSource(void)
{
	AssertThis(0);
	return _pgstSource;
}


/***************************************************************************
	If there is an stn for the given sid in _pgstSource, return the
	location of the stn.
***************************************************************************/
bool TAGM::_FFindSid(long sid, long *pistn)
{
	AssertThis(0);
	Assert(sid >= 0, "Invalid sid");
	AssertNilOrVarMem(pistn);

	long istn;
	long sidT;

	for (istn = 0; istn < _pgstSource->IvMac(); istn++)
		{
		_pgstSource->GetExtra(istn, &sidT);
		if (sid == sidT)  // it's already in there
			{
			if (pvNil != pistn)
				*pistn = istn;
			return fTrue;
			}
		}
	TrashVar(pistn);
	return fFalse;
}


/***************************************************************************
	Add source title string table entries to tag manager, if it doesn't
	already know them.
***************************************************************************/
bool TAGM::FMergeGstSource(PGST pgst, short bo, short osk)
{
	AssertThis(0);
	AssertPo(pgst, 0);
	Assert(size(long) == pgst->CbExtra(), "bad pgstSource");

	long istn;
	STN stn;
	long sid;

	for (istn = 0; istn < pgst->IvMac(); istn++)
		{
		pgst->GetStn(istn, &stn);
		pgst->GetExtra(istn, &sid);
		if (kboOther == bo)
			SwapBytesBom(&sid, kbomSid);
		if (!FAddStnSource(&stn, sid))
			return fFalse;
		}

	return fTrue;
}


/***************************************************************************
	Add source title string to tag manager, if it's not already there
***************************************************************************/
bool TAGM::FAddStnSource(PSTN pstn, long sid)
{
	AssertThis(0);
	AssertPo(pstn, 0);
	Assert(sid >= 0, "Invalid sid");
	
	if (_FFindSid(sid))
		return fTrue; // String is already there

	return _pgstSource->FAddStn(pstn, &sid); // Try to add it
}


/***************************************************************************
	Find the sid with the given string as its source name.  pstn can be
	the merged name, the short name, or the long name.
***************************************************************************/
bool TAGM::FGetSid(PSTN pstn, long *psid)
{
	AssertThis(0);
	AssertPo(pstn, 0);
	AssertVarMem(psid);

	long istn;
	STN stnMerged;
	STN stnLong;
	STN stnShort;

	for (istn = 0; istn < _pgstSource->IvMac(); istn++)
		{
		_pgstSource->GetStn(istn, &stnMerged);
		if (stnMerged.FEqualUser(pstn, fstnIgnoreCase))
			{
			_pgstSource->GetExtra(istn, psid);
			return fTrue;
			}
		SplitString(&stnMerged, &stnLong, &stnShort);
		if (stnLong.FEqualUser(pstn, fstnIgnoreCase) ||
			stnShort.FEqualUser(pstn, fstnIgnoreCase))
			{
			_pgstSource->GetExtra(istn, psid);
			return fTrue;
			}
		}
	return fFalse;
}


/***************************************************************************
	Find the string of the source with the given sid	
***************************************************************************/
bool TAGM::_FGetStnMergedOfSid(long sid, PSTN pstn)
{
	AssertThis(0);
	AssertVarMem(pstn);

	long istn;
	long sidT;

	for (istn = 0; istn < _pgstSource->IvMac(); istn++)
		{
		_pgstSource->GetStn(istn, pstn);
		_pgstSource->GetExtra(istn, &sidT);
		if (sid == sidT)
			return fTrue;
		}
	return fFalse;
}


/***************************************************************************
	Find the string of the source with the given sid	
***************************************************************************/
bool TAGM::_FGetStnSplitOfSid(long sid, PSTN pstnLong, PSTN pstnShort)
{
	AssertThis(0);
	AssertVarMem(pstnLong);
	AssertVarMem(pstnShort);

	STN stnMerged;

	if (!_FGetStnMergedOfSid(sid, &stnMerged))
		return fFalse;
	SplitString(&stnMerged, pstnLong, pstnShort);
	return fTrue;
}


/***************************************************************************
	Builds the FNI to the HD files for a given sid
	- If we don't even know the string for the sid, return fFalse
	- If there is no fniHD, set *pfExists to fFalse and return fTrue
	- If we find the fniHD, put it in *pfniHD, set *pfExists to fTrue,
	  and return fTrue
***************************************************************************/
bool TAGM::_FBuildFniHD(long sid, PFNI pfniHD, bool *pfExists)
{
	AssertThis(0);
	Assert(sid >= 0, "Invalid sid");
	AssertVarMem(pfniHD);
	AssertVarMem(pfExists);

	STN stnLong;
	STN stnShort;
	FNI fni;

	*pfExists = fFalse;
	if (!_FGetStnSplitOfSid(sid, &stnLong, &stnShort))
		{
		return fFalse; // can't even determine if fniHD exists or not
		}

	fni = _fniHDRoot;
	if (!fni.FDownDir(&stnLong, ffniMoveToDir) &&
		!fni.FDownDir(&stnShort, ffniMoveToDir))
		{
		return fTrue; // fniHD doesn't exist
		}
	*pfniHD = fni;
	*pfExists = fTrue;
	return fTrue;
}


/***************************************************************************
	See if there are any content files in the directory specified by pfni
***************************************************************************/
bool TAGM::_FDetermineIfContentOnFni(PFNI pfni, bool *pfContentOnFni)
{
	AssertThis(0);
	AssertPo(pfni, ffniDir);
	AssertVarMem(pfContentOnFni);

	FNE fne;
	FTG ftgContent = kftgContent;
	FNI fni;

	if (!fne.FInit(pfni, &ftgContent, 1))
		return fFalse;
	if (fne.FNextFni(&fni))
		*pfContentOnFni = fTrue;
	else
		*pfContentOnFni = fFalse;
	return fTrue;
}


/***************************************************************************
	Returns whether the directory pointed to by pfniCD exists where we
	think it does.  Or, if pstn is non-nil, try to go down from pfniCD
	to pstn.
***************************************************************************/
bool TAGM::_FEnsureFniCD(long sid, FNI *pfniCD, PSTN pstn)
{
	AssertThis(0);
	Assert(sid >= 0, "Invalid sid");
	AssertPo(pfniCD, ffniDir);
	AssertNilOrPo(pstn, 0);

	ERS ersT;
	ERS *pers;
	bool fRet;

	pers = vpers;
	vpers = &ersT;

	#ifdef WIN
		// Block the Windows "There is no disk in the drive" message
		UINT em;
		em = SetErrorMode(SEM_FAILCRITICALERRORS);
	#endif // WIN

	if (pvNil != pstn)
		fRet = pfniCD->FDownDir(pstn, ffniMoveToDir);
	else
		fRet = (pfniCD->TExists() == tYes);

	#ifdef WIN
		SetErrorMode(em); // restore the error mode
	#endif // WIN

	vpers = pers;
	return fRet;
}


/***************************************************************************
	This function verifies that the source (e.g., CD) is where we think it
	is, and searches for it otherwise.  Pass the previously determined FNI
	of the CD directory file in pfniCD.  Or if this is the first time
	looking for this source, pass in any FNI with a FTG of ftgNil.	If it
	can't find the CD directory, it returns fFalse with pfniInfo untouched.
***************************************************************************/
bool TAGM::_FFindFniCD(long sid, PFNI pfniCD, bool *pfFniChanged)
{
	AssertThis(0);
	Assert(sid >= 0, "Invalid sid");
	AssertPo(pfniCD, ffniEmpty | ffniDir); // could be a blank FNI
	AssertVarMem(pfFniChanged);

	FNE fne;
	FNI fni;
	STN stnLong;
	STN stnShort;
	FNI fniCD;

	*pfFniChanged = fFalse;

	// If pfniCD's ftg is not ftgNil, we've opened this source before,
	// so look for it where it was last time.
	if (ftgNil != pfniCD->Ftg())
		{
		if (_FEnsureFniCD(sid, pfniCD))
			{
			// The source is where we thought it was
			return fTrue;
			}
		else
			{
			// With the way the CRM stuff works now, the CRM can't
			// move to another path.  So fail if the CD isn't exactly
			// where it was before.
			return fFalse;
			}
		}

	if (!_FGetStnSplitOfSid(sid, &stnLong, &stnShort))
		return fFalse;
	// The source has moved/disappeared, or we're opening it for the first
	// time, so search all drives for it.
	if (!fne.FInit(pvNil, pvNil, 0))
		return fFalse;
	while (fne.FNextFni(&fni))
		{
		if (fni.Grfvk() & fvkFloppy) // don't buzz floppies
			continue;
		fniCD = fni;
		if (!_FEnsureFniCD(sid, &fniCD, &stnShort) &&
			!_FEnsureFniCD(sid, &fniCD, &stnLong))
			{
			continue;
			}
		*pfFniChanged = fTrue;
		*pfniCD = fniCD;
		return fTrue;
		}
	// Couldn't find the source
	return fFalse;
}


/***************************************************************************
	Calls the client-supplied callback, which should tell the user that
	the source named stn cannot be found.  Returns fTrue if the user wants
	to retry, else fFalse.	
***************************************************************************/
bool TAGM::_FRetry(long sid)
{
	AssertThis(0);
	Assert(sid >= 0, "Invalid sid");

	STN stnLong;
	STN stnShort;

	if (!_FGetStnSplitOfSid(sid, &stnLong, &stnShort))
		{
		Bug("_pgstSource has no string for this sid!");
		return fFalse;
		}
	return _pfninscd(&stnLong);
}


/***************************************************************************
	Builds the CRM for the given sid's source.  pfniDir tells where the
	content files are.	
***************************************************************************/
PCRM TAGM::_PcrmSourceNew(long sid, PFNI pfniDir)
{
	AssertThis(0);
	Assert(sid >= 0, "Invalid sid");
	AssertPo(pfniDir, ffniDir);

	STN stn;
	FNI fni;
	PCRM pcrmSource = pvNil;
	FNE fne;
	FTG ftgChk = kftgContent;
	PCFL pcfl = pvNil;

	pcrmSource = CRM::PcrmNew(0);
	if (pvNil == pcrmSource)
		goto LFail;

	// Add all chunky files in content directory to pcrmSource
	if (!fne.FInit(pfniDir, &ftgChk, 1))
		goto LFail;
	while (fne.FNextFni(&fni))
		{
		pcfl = CFL::PcflOpen(&fni, fcflNil);
		if (pvNil == pcfl)
			goto LFail;
		if (!pcrmSource->FAddCfl(pcfl, _cbCache))
			goto LFail;
		ReleasePpo(&pcfl);
		}
	return pcrmSource;
LFail:
	ReleasePpo(&pcfl);
	ReleasePpo(&pcrmSource);
	return pvNil;
}


/***************************************************************************
	Returns the source CRM for the given sid, creating (and remembering) a
	new one if there isn't one already.  It verifies that the CD is still
	in the drive, unless fDontHitCD is fTrue.
***************************************************************************/
PCRM TAGM::_PcrmSourceGet(long sid, bool fDontHitCD)
{
	AssertThis(0);
	Assert(sid >= 0, "Invalid sid");

	long isfs;
	SFS sfs;
	bool fContentOnFni;
	bool fFniChanged;
	bool fExists;

	for (isfs = 0; isfs < _pglsfs->IvMac(); isfs++)
		{
		_pglsfs->Get(isfs, &sfs);
		if (sid == sfs.sid)
			goto LSetupSfs;
		}
	// SFS for this sid doesn't exist in _pglsfs, so make one
	sfs.Clear();
	sfs.sid = sid;
	if (!_pglsfs->FAdd(&sfs, &isfs))
		return pvNil;
LSetupSfs:
	if (sfs.tContentOnHD == tMaybe)
		{
		if (sfs.fniHD.Ftg() == ftgNil)
			{
			if (!_FBuildFniHD(sid, &sfs.fniHD, &fExists))
				return pvNil;
			if (!fExists)
				sfs.tContentOnHD = tNo;
			_pglsfs->Put(isfs, &sfs);
			}
		if (sfs.tContentOnHD == tMaybe)
			{
			if (!_FDetermineIfContentOnFni(&sfs.fniHD, &fContentOnFni))
				return pvNil;
			sfs.tContentOnHD = (fContentOnFni ? tYes : tNo);
			_pglsfs->Put(isfs, &sfs);
			}
		}
	if (tYes == sfs.tContentOnHD)
		{
		if (pvNil == sfs.pcrmSource)
			{
			sfs.pcrmSource = _PcrmSourceNew(sid, &sfs.fniHD);
			if (pvNil == sfs.pcrmSource)
				return pvNil;
			_pglsfs->Put(isfs, &sfs);
			}
		return sfs.pcrmSource;
		}

	// Else content is not on HD, so look at CD
	if (!fDontHitCD)
		{
		// Verify that CD is where we thought it was, or find
		// it if we haven't found it before
		while (!_FFindFniCD(sid, &sfs.fniCD, &fFniChanged))
			{
			// Ask user to insert the CD
			if (!_FRetry(sid))
				return pvNil;
			}
		if (fFniChanged)
			{
			Assert(sfs.pcrmSource == pvNil, 
				"fniCD can't change once pcrm is opened!");
			_pglsfs->Put(isfs, &sfs); // update sfs.fniCD
			}
		}
	if (pvNil == sfs.pcrmSource)
		{
		if (fDontHitCD)
			{
			Bug("should have valid pcrmSource!");
			}
		else
			{
			sfs.pcrmSource = _PcrmSourceNew(sid, &sfs.fniCD);
			if (pvNil == sfs.pcrmSource)
				return pvNil;
			_pglsfs->Put(isfs, &sfs);
			}
		}
	return sfs.pcrmSource;
}


/***************************************************************************
	Determines whether source is on HD (if it is, don't cache its stuff to
	HD!)  Note that the function return value is whether the function
	completed without error, not whether the source is on HD.
***************************************************************************/
bool TAGM::_FDetermineIfSourceHD(long sid, bool *pfIsOnHD)
{
	AssertThis(0);
	Assert(sid >= 0, "Invalid sid");
	AssertVarMem(pfIsOnHD);

	long isfs;
	SFS sfs;
	bool fContentOnFni;
	bool fExists;

	for (isfs = 0; isfs < _pglsfs->IvMac(); isfs++)
		{
		_pglsfs->Get(isfs, &sfs);
		if (sid == sfs.sid)
			goto LSetupSfs;
		}
	// SFS for this sid doesn't exist in _pglsfs, so make one
	sfs.Clear();
	sfs.sid = sid;
	if (!_pglsfs->FAdd(&sfs, &isfs))
		return fFalse;
LSetupSfs:
	if (sfs.tContentOnHD == tMaybe)
		{
		if (sfs.fniHD.Ftg() == ftgNil)
			{
			if (!_FBuildFniHD(sid, &sfs.fniHD, &fExists))
				return fFalse;
			if (!fExists)
				sfs.tContentOnHD = tNo;
			_pglsfs->Put(isfs, &sfs);
			}
		if (sfs.tContentOnHD == tMaybe)
			{
			if (!_FDetermineIfContentOnFni(&sfs.fniHD, &fContentOnFni))
				return fFalse;
			sfs.tContentOnHD = (fContentOnFni ? tYes : tNo);
			_pglsfs->Put(isfs, &sfs);
			}
		}
	*pfIsOnHD = (sfs.tContentOnHD == tYes);
	return fTrue;
}


/***************************************************************************
	Get the FNI for the HD directory
***************************************************************************/
bool TAGM::_FGetFniHD(long sid, PFNI pfniHD)
{
	AssertThis(0);
	AssertVarMem(pfniHD);

	long isfs;
	SFS sfs;
	bool fExists;

	for (isfs = 0; isfs < _pglsfs->IvMac(); isfs++)
		{
		_pglsfs->Get(isfs, &sfs);
		if (sid == sfs.sid)
			goto LSetupSFS;
		}
	// SFS for this sid doesn't exist in _pglsfs, so make one
	sfs.Clear();
	sfs.sid = sid;
	if (!_pglsfs->FAdd(&sfs, &isfs))
		return fFalse;
LSetupSFS:
	if (sfs.fniHD.Ftg() == ftgNil)
		{
		if (!_FBuildFniHD(sid, &sfs.fniHD, &fExists) || !fExists)
			return fFalse;
		}
	_pglsfs->Put(isfs, &sfs);
	*pfniHD = sfs.fniHD;
	return fTrue;
}


/***************************************************************************
	Get the FNI for the CD directory
***************************************************************************/
bool TAGM::_FGetFniCD(long sid, PFNI pfniCD, bool fAskForCD)
{
	AssertThis(0);
	AssertVarMem(pfniCD);

	long isfs;
	SFS sfs;
	bool fFniChanged;

	for (isfs = 0; isfs < _pglsfs->IvMac(); isfs++)
		{
		_pglsfs->Get(isfs, &sfs);
		if (sid == sfs.sid)
			goto LSetupSFS;
		}
	// SFS for this sid doesn't exist in _pglsfs, so make one
	sfs.Clear();
	sfs.sid = sid;
	if (!_pglsfs->FAdd(&sfs, &isfs))
		return fFalse;
LSetupSFS:
	while (!_FFindFniCD(sid, &sfs.fniCD, &fFniChanged))
		{
		if (!fAskForCD)
			return fFalse;
		if (!_FRetry(sid))
			return fFalse;
		}
	if (fFniChanged)
		_pglsfs->Put(isfs, &sfs);
	*pfniCD = sfs.fniCD;
	return fTrue;
}


/***************************************************************************
	Finds the file with name pstn on the HD or CD.
***************************************************************************/
bool TAGM::FFindFile(long sid, PSTN pstn, PFNI pfni, bool fAskForCD)
{
	AssertThis(0);
	Assert(sid >= 0, "Invalid sid");
	AssertPo(pstn, 0);
	AssertVarMem(pfni);

	FTG ftg;

	if (!pfni->FBuildFromPath(pstn))
		return fFalse;
	ftg = pfni->Ftg();

	// First, look on the HD
	if (!_FGetFniHD(sid, pfni))
		return fFalse;
	if (pfni->FSetLeaf(pstn, ftg) && tYes == pfni->TExists())
		return fTrue;

	// Now look on the CD, asking for it if fAskForCD
	if (!_FGetFniCD(sid, pfni, fAskForCD))
		return fFalse;
	if (pfni->FSetLeaf(pstn, ftg) && tYes == pfni->TExists())
		return fTrue;

	pfni->SetNil();
	return fFalse; // file not found
}


/***************************************************************************
	Build a tag for a child of another tag.  Note that this may hit the 
	CD if _PcrmSourceGet has not yet been called for ptagPar->sid.
***************************************************************************/
bool TAGM::FBuildChildTag(PTAG ptagPar, CHID chid, CTG ctgChild, 
	PTAG ptagChild)
{
	AssertThis(0);
	AssertVarMem(ptagPar);
	Assert(ptagPar->sid >= 0, "Invalid sid");
	AssertVarMem(ptagChild);

	PCRM pcrmSource;
	PCRF pcrfSource;
	KID kid;

	TrashVar(ptagChild);

	if (ksidUseCrf == ptagPar->sid)
		{
		AssertPo(ptagPar->pcrf, 0);
		if (!ptagPar->pcrf->Pcfl()->FGetKidChidCtg(ptagPar->ctg, 
			ptagPar->cno, chid, ctgChild, &kid))
			{
			return fFalse; // child chunk not found
			}
		ptagChild->sid = ksidUseCrf;
		ptagChild->pcrf = ptagPar->pcrf;
		ptagPar->pcrf->AddRef();
		ptagChild->ctg = kid.cki.ctg;
		ptagChild->cno = kid.cki.cno;
		return fTrue;
		}
	pcrmSource = _PcrmSourceGet(ptagPar->sid);
	if (pvNil == pcrmSource)
		return fFalse;
	pcrfSource = pcrmSource->PcrfFindChunk(ptagPar->ctg, ptagPar->cno);
	if (pvNil == pcrfSource)
		return fFalse; // parent chunk not found
	if (!pcrfSource->Pcfl()->FGetKidChidCtg(ptagPar->ctg, ptagPar->cno, chid, 
		ctgChild, &kid))
		{
		return fFalse; // child chunk not found
		}
	ptagChild->sid = ptagPar->sid;
	ptagChild->ctg = kid.cki.ctg;
	ptagChild->cno = kid.cki.cno;
	
	return fTrue;	
}


/***************************************************************************
	Put specified chunk in cache file, if it's not there yet
***************************************************************************/
bool TAGM::FCacheTagToHD(PTAG ptag, bool fCacheChildChunks)
{
	AssertThis(0);
	AssertVarMem(ptag);
	Assert(ptag->sid >= 0, "Invalid sid");

	PCRM pcrmSource;
	PCRF pcrfSource;
	bool fSourceIsOnHD;
	PCFL pcfl;

	if (ksidUseCrf == ptag->sid)
		return fTrue;

	// Do nothing if the source itself is already on HD
	if (!_FDetermineIfSourceHD(ptag->sid, &fSourceIsOnHD))
		goto LFail;
	if (fSourceIsOnHD)
		return fTrue;

	pcrmSource = _PcrmSourceGet(ptag->sid);
	if (pvNil == pcrmSource)
		goto LFail;
	pcrfSource = pcrmSource->PcrfFindChunk(ptag->ctg, ptag->cno);
	if (pvNil == pcrfSource)
		goto LFail;	// chunk not found

	pcfl = pcrfSource->Pcfl();
	if (fCacheChildChunks)
		{
		// Cache the chunk specified by the tag, and all its child
		// chunks.  
		CGE cge;
		KID kid;
		ulong grfcgeIn = 0;
		ulong grfcgeOut;

		cge.Init(pcfl, ptag->ctg, ptag->cno);
		while (cge.FNextKid(&kid, pvNil, &grfcgeOut, grfcgeIn))
			{
			if (grfcgeOut & fcgePre)
				{
				if (!pcfl->FEnsureOnExtra(kid.cki.ctg, kid.cki.cno))
					goto LFail;
				}
			}

		}
	else
		{
		// Just cache the chunk specified by the tag
		if (!pcfl->FEnsureOnExtra(ptag->ctg, ptag->cno))
			goto LFail;
		}
	return fTrue;
LFail:
	PushErc(ercSocCantCacheTag);
	return fFalse;
}


/***************************************************************************
	Resolve the TAG to a BACO.  Only use HD cache files, unless fUseCD is
	fTrue.
***************************************************************************/
PBACO TAGM::PbacoFetch(PTAG ptag, PFNRPO pfnrpo, bool fUseCD)
{
	AssertThis(0);
	AssertVarMem(ptag);
	Assert(ptag->sid >= 0, "Invalid sid");
	Assert(pvNil != pfnrpo, "bad rpo");

	PBACO pbaco = pvNil;
	PCRM pcrmSource;

	if (ptag->sid == ksidUseCrf)
		{
		// Tag knows pcrf, so just read from there.  Nothing we can do if
		// it's not there.
		AssertPo(ptag->pcrf, 0);
		return ptag->pcrf->PbacoFetch(ptag->ctg, ptag->cno, pfnrpo);
		}

	// fTrue parameter ensures that _PcrmSourceGet won't hit the CD
	pcrmSource = _PcrmSourceGet(ptag->sid, fTrue);
	if (pvNil == pcrmSource)
		return pvNil;

	pbaco = pcrmSource->PbacoFetch(ptag->ctg, ptag->cno, pfnrpo);
	return pbaco;
}


/***************************************************************************
	Clear the cache for source sid.  If sid is sidNil, clear all caches.
***************************************************************************/
void TAGM::ClearCache(long sid, ulong grftagm)
{
	AssertThis(0);
	Assert(sid >= 0, "Invalid sid");

	long isfs, isfsMac;
	long cbMax;

	vpappb->BeginLongOp();

	isfsMac = _pglsfs->IvMac();
	for (isfs = 0; isfs < isfsMac; isfs++)
		{
		long icrf, icrfMac;
		SFS sfs;
		PCRM pcrmSource;

		_pglsfs->Get(isfs, &sfs);
		if ((sid != sidNil && sfs.sid != sid) || sfs.pcrmSource == pvNil)
			continue;
		if (grftagm & ftagmFile)
			{
			// The following line may seem silly, since we already have
			// sfs.pcrmSource.  But it ensures that the crm's CD is inserted, 
			// since the FReopen() will need the CD to be in.
			pcrmSource = _PcrmSourceGet(sfs.sid);
			if (pvNil == pcrmSource)
				continue;
			}
		else
			{
			// Just doing a memory purge, so sfs.pcrmSource is valid.
			pcrmSource = sfs.pcrmSource;
			}
		icrfMac = pcrmSource->Ccrf();
		for (icrf = 0; icrf < icrfMac; icrf++)
			{
			PCRF pcrf;

			pcrf = pcrmSource->PcrfGet(icrf);
			AssertPo(pcrf->Pcfl(), 0);
			if (pcrf->Pcfl() != pvNil)
				{
				if (grftagm & ftagmFile)
					{
					// Clear the HD cache by reopening the file
					pcrf->Pcfl()->FReopen();  // Ignore error
					}
				if (grftagm & ftagmMemory)
					{
					// Clear RAM cache (for BACOs with 0 cactRef) by
					// temporarily setting the CRF's cbMax to 0
					cbMax = pcrf->CbMax();
					pcrf->SetCbMax(0);
					pcrf->SetCbMax(cbMax);
					}
				}
			}
		}

	vpappb->EndLongOp();
}


/***************************************************************************
	Prepares this tag to be used (resolved).  The tag is invalid until you
	call this, *except* you can pass the tag to FCacheTagToHD() before
	calling FOpenTag().  If you FOpenTag() a tag, you must CloseTag() it
	when you're done with it.
***************************************************************************/
bool TAGM::FOpenTag(PTAG ptag, PCRF pcrfDest, PCFL pcflSrc)
{
	AssertVarMem(ptag);
	Assert(ptag->sid >= 0, "Invalid sid");
	AssertPo(pcrfDest, 0);
	AssertNilOrPo(pcflSrc, 0);

	CNO cnoDest;	

	if (ptag->sid != ksidUseCrf)
		return fTrue;
	if (pvNil != pcflSrc && pcrfDest->Pcfl() != pcflSrc)
		{
		if (!pcflSrc->FCopy(ptag->ctg, ptag->cno, pcrfDest->Pcfl(),	
			&cnoDest))
			{
			ptag->pcrf = pvNil;
			return fFalse; // copy failed
			}
		ptag->cno = cnoDest;
		}
	ptag->pcrf = pcrfDest;
	pcrfDest->AddRef();
	return fTrue;
}


/***************************************************************************
	Save tag's data in the given CRF.  If fRedirect, the tag now points
	to the copy in the CRF.
***************************************************************************/
bool TAGM::FSaveTag(PTAG ptag, PCRF pcrf, bool fRedirect)
{
	AssertVarMem(ptag);
	Assert(ptag->sid >= 0, "Invalid sid");
	AssertPo(pcrf, 0);

	CNO cnoDest;
		
	if (ptag->sid != ksidUseCrf)
		return fTrue;

	AssertPo(ptag->pcrf, 0);
		
	if (!ptag->pcrf->Pcfl()->FCopy(ptag->ctg, ptag->cno, pcrf->Pcfl(),
		&cnoDest))
		{
		return fFalse; // copy failed
		}
	
	if (fRedirect)	
		{
		pcrf->AddRef();
		ReleasePpo(&ptag->pcrf);
		ptag->pcrf = pcrf;
		ptag->cno = cnoDest;
		}

	return fTrue;
}	


/***************************************************************************
	Call this for each tag when you're duplicating it.  Increments
	refcount on the tag's CRF.
***************************************************************************/
void TAGM::DupTag(PTAG ptag)
{
	AssertVarMem(ptag);
	Assert(ptag->sid >= 0, "Invalid sid");

	if (ptag->sid == ksidUseCrf)
		{
		AssertPo(ptag->pcrf, 0);
		ptag->pcrf->AddRef();
		}
}


/***************************************************************************
	Close the tag
***************************************************************************/
void TAGM::CloseTag(PTAG ptag)
{
	AssertVarMem(ptag);
	// Client destructors often call CloseTag on an uninitialized tag, so
	// don't Assert on ksidInvalid tags...just ignore them
	Assert(ptag->sid == ksidInvalid || ptag->sid >= 0, "Invalid sid");

	if (ptag->sid == ksidUseCrf)
		{
		AssertPo(ptag->pcrf, 0);
		ReleasePpo(&ptag->pcrf);
		}
}


/***************************************************************************
	Compare two tags.  Tags are sorted first by sid, then CTG, then CNO.
***************************************************************************/
ulong TAGM::FcmpCompareTags(PTAG ptag1, PTAG ptag2)
{
	AssertVarMem(ptag1);
	Assert(ptag1->sid >= 0, "Invalid sid");
	AssertVarMem(ptag2);
	Assert(ptag2->sid >= 0, "Invalid sid");

    if (ptag1->sid < ptag2->sid)
		return fcmpLt;
    if (ptag1->sid > ptag2->sid)
		return fcmpGt;
    if (ptag1->ctg < ptag2->ctg)
		return fcmpLt;
    if (ptag1->ctg > ptag2->ctg)
		return fcmpGt;
    if (ptag1->cno < ptag2->cno)
		return fcmpLt;
    if (ptag1->cno > ptag2->cno)
		return fcmpGt;
	// If both sids are ksidUseCrf, compare CRFs
	if (ptag1->sid == ksidUseCrf) // implies ptag2->sid == ksidUseCrf
		{
	    if (ptag1->pcrf < ptag2->pcrf)
			return fcmpLt;
	    if (ptag1->pcrf > ptag2->pcrf)
			return fcmpGt;
		}
	return fcmpEq;
}


#ifdef DEBUG
/***************************************************************************
	Assert the validity of the TAGM.
***************************************************************************/
void TAGM::AssertValid(ulong grf)
{
	long isfs;
	SFS sfs;

    TAGM_PAR::AssertValid(fobjAllocated);
	AssertPo(_pglsfs, 0);
	AssertPo(_pgstSource, 0);
	Assert(pvNil != _pfninscd, "bad _pfninscd");

	for (isfs = 0; isfs < _pglsfs->IvMac(); isfs++)
		{
		_pglsfs->Get(isfs, &sfs);
		AssertPo(&sfs.fniHD, ffniDir | ffniEmpty);
		AssertPo(&sfs.fniCD, ffniDir | ffniEmpty);
		AssertNilOrPo(sfs.pcrmSource, 0);
		}
}


/***************************************************************************
	Mark memory used by the TAGM.
***************************************************************************/
void TAGM::MarkMem(void)
{
	AssertThis(0);

	long isfs;
	SFS sfs;

	TAGM_PAR::MarkMem();
	MarkMemObj(_pglsfs);
	MarkMemObj(_pgstSource);
	for (isfs = 0; isfs < _pglsfs->IvMac(); isfs++)
		{
		_pglsfs->Get(isfs, &sfs);
		MarkMemObj(sfs.pcrmSource);
		}
}
#endif // DEBUG


#ifdef DEBUG
/***************************************************************************
	Mark memory used by the TAG.
***************************************************************************/
void TAG::MarkMem(void)
{
	if (sid == ksidUseCrf)
		{
		AssertPo(pcrf, 0);
		MarkMemObj(pcrf);
		}
}
#endif // DEBUG
